/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.examples.source;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.uwyn.jhighlight.renderer.Renderer;
import com.uwyn.jhighlight.renderer.XhtmlRendererFactory;
import org.apache.wicket.Component;
import org.apache.wicket.Page;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxCallListener;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.authorization.UnauthorizedInstantiationException;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.request.http.handler.ErrorCodeRequestHandler;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.io.IOUtils;
import org.apache.wicket.util.io.Streams;
import org.apache.wicket.util.lang.PackageName;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Displays the resources in a packages directory in a browsable format.
 * 
 * @author Martijn Dashorst
 */
public class SourcesPage extends WebPage
{
	private static final Logger log = LoggerFactory.getLogger(SourcesPage.class);

	/**
	 * Model for retrieving the source code from the classpath of a packaged resource.
	 */
	public class SourceModel extends AbstractReadOnlyModel<String>
	{
		/**
		 * Constructor.
		 */
		public SourceModel()
		{
		}

		/**
		 * Returns the contents of the file loaded from the classpath.
		 * 
		 * @return the contents of the file identified by name
		 */
		@Override
		public String getObject()
		{
			// name contains the name of the selected file
			if (Strings.isEmpty(name) &&
				Strings.isEmpty(getPage().getRequest()
					.getRequestParameters()
					.getParameterValue(SOURCE)
					.toOptionalString()))
			{
				return "";
			}
			BufferedReader br = null;
			String source = null;
			try
			{
			 StringBuilder sb = new StringBuilder();
				source = (name != null) ? name : getPage().getRequest()
					.getRequestParameters()
					.getParameterValue(SOURCE)
					.toOptionalString();
				InputStream resourceAsStream = getPageTargetClass().getResourceAsStream(source);
				if (resourceAsStream == null)
				{
					return "Unable to read the source for " + source;
				}
				br = new BufferedReader(new InputStreamReader(resourceAsStream));

				while (br.ready())
				{
					sb.append(br.readLine());
					sb.append("\n");
				}
				int lastDot = source.lastIndexOf('.');
				if (lastDot != -1)
				{
					String type = source.substring(lastDot + 1);
					Renderer renderer = XhtmlRendererFactory.getRenderer(type);
					if (renderer != null)
					{
						return renderer.highlight(source, sb.toString(), "UTF-8", true);
					}
				}
				return Strings.escapeMarkup(sb.toString(), false, true)
					.toString()
					.replaceAll("\n", "<br />");
			}
			catch (IOException e)
			{
				log.error(
					"Unable to read resource stream for: " + source + "; Page=" + page.toString(),
					e);
				return "";
			}
			finally
			{
				IOUtils.closeQuietly(br);
			}
		}
	}

	/**
	 * Model for retrieving the contents of a package directory from the class path.
	 */
	public class PackagedResourcesModel extends AbstractReadOnlyModel<List<String>>
	{
		private final List<String> resources = new ArrayList<String>();

		/**
		 * Constructor.
		 */
		public PackagedResourcesModel()
		{
		}

		/**
		 * Clears the list to save space.
		 */
		protected void onDetach()
		{
			resources.clear();
		}

		/**
		 * Returns the list of resources found in the package of the page.
		 * 
		 * @return the list of resources found in the package of the page.
		 */
		@Override
		public List<String> getObject()
		{
			if (resources.isEmpty())
			{
				get(getPageTargetClass());
// PackageName name = PackageName.forClass(page);
// ClassLoader loader = page.getClassLoader();
// String path = Strings.replaceAll(name.getName(), ".", "/").toString();
// try
// {
// // gives the urls for each place where the package
// // path could be found. There could be multiple
// // jar files containing the same package, so each
// // jar file has its own url.
//
// Enumeration<URL> urls = loader.getResources(path);
// while (urls.hasMoreElements())
// {
// URL url = urls.nextElement();
//
// // the url points to the directory structure
// // embedded in the classpath.
//
// getPackageContents(url);
// }
// }
// catch (IOException e)
// {
// log.error("Unable to read resource for: " + path, e);
// }
			}
			return resources;
		}

		/**
		 * Retrieves the package contents for the given URL.
		 * 
		 * @param packageListing
		 *            the url to list.
		 */
		private void getPackageContents(URL packageListing)
		{
			BufferedReader br = null;
			try
			{
				InputStream openStream = Streams.readNonCaching(packageListing);
				if (openStream == null)
				{
					return;
				}
				br = new BufferedReader(new InputStreamReader(openStream));

				while (br.ready())
				{
					String listing = br.readLine();
					String extension = Strings.afterLast(listing, '.');
					if (!listing.endsWith("class"))
					{
						resources.add(listing);
					}
				}
			}
			catch (IOException e)
			{
				log.error("Unable to get package content: " + packageListing.toString(), e);
			}
			finally
			{
				IOUtils.closeQuietly(br);
			}
		}

		private final void addResources(final Class<?> scope,
			final AppendingStringBuffer relativePath, final File dir)
		{
			File[] files = dir.listFiles();
			for (File file : files)
			{
				if (file.isDirectory())
				{
					addResources(scope,
						new AppendingStringBuffer(relativePath).append(file.getName()).append('/'),
						file);
				}
				else
				{
					String name = file.getName();
					String extension = Strings.afterLast(name, '.');
					if (!name.endsWith("class"))
					{
						resources.add(relativePath + name);
					}
				}
			}
		}

		private void get(Class<?> scope)
		{
			String packageRef = Strings.replaceAll(PackageName.forClass(scope).getName(), ".", "/")
				.toString();
			ClassLoader loader = scope.getClassLoader();
			try
			{
				// loop through the resources of the package
				Enumeration<URL> packageResources = loader.getResources(packageRef);
				while (packageResources.hasMoreElements())
				{
					URL resource = packageResources.nextElement();
					URLConnection connection = resource.openConnection();
					if (connection instanceof JarURLConnection)
					{
						JarFile jf = ((JarURLConnection)connection).getJarFile();
						scanJarFile(scope, packageRef, jf);
					}
					else
					{
						String absolutePath = scope.getResource("").toExternalForm();
						File basedir;
						URI uri;
						try
						{
							uri = new URI(absolutePath);
						}
						catch (URISyntaxException e)
						{
							throw new RuntimeException(e);
						}
						try
						{
							basedir = new File(uri);
						}
						catch (IllegalArgumentException e)
						{
							log.debug("Can't construct the uri as a file: " + absolutePath);
							// if this is throwen then the path is not really a
							// file. but could be a zip.
							String jarZipPart = uri.getSchemeSpecificPart();
							// lowercased for testing if jar/zip, but leave the
							// real filespec unchanged
							String lowerJarZipPart = jarZipPart.toLowerCase();
							int index = lowerJarZipPart.indexOf(".zip");
							if (index == -1)
								index = lowerJarZipPart.indexOf(".jar");
							if (index == -1)
								throw e;

							String filename = jarZipPart.substring(0, index + 4); // 4 =
							// len
							// of
							// ".jar"
							// or
							// ".zip"
							log.debug("trying the filename: " + filename + " to load as a zip/jar.");
							JarFile jarFile = new JarFile(filename, false);
							scanJarFile(scope, packageRef, jarFile);
							return;
						}
						if (!basedir.isDirectory())
						{
							throw new IllegalStateException(
								"unable to read resources from directory " + basedir);
						}
						addResources(scope, new AppendingStringBuffer(), basedir);
					}
				}
			}
			catch (IOException e)
			{
				throw new WicketRuntimeException(e);
			}
			Collections.sort(resources);
		}

		private void scanJarFile(Class<?> scope, String packageRef, JarFile jf)
		{
			Enumeration<JarEntry> enumeration = jf.entries();
			while (enumeration.hasMoreElements())
			{
				JarEntry je = enumeration.nextElement();
				String name = je.getName();
				if (name.startsWith(packageRef))
				{
					name = name.substring(packageRef.length() + 1);
					String extension = Strings.afterLast(name, '.');
					if (!name.endsWith("class"))
					{
						resources.add(name);
					}
				}
			}
		}
	}

	/**
	 * Displays the resources embedded in a package in a list.
	 */
	public class FilesBrowser extends WebMarkupContainer
	{
		/**
		 * Constructor.
		 * 
		 * @param id
		 *            the component identifier
		 */
		public FilesBrowser(String id)
		{
			super(id);
			ListView<String> lv = new ListView<String>("file", new PackagedResourcesModel())
			{
				@Override
				protected void populateItem(final ListItem<String> item)
				{
					AjaxFallbackLink<String> link = new AjaxFallbackLink<String>("link",
						item.getModel())
					{
						@Override
						public void onClick(AjaxRequestTarget target)
						{
							setName(getDefaultModelObjectAsString());

							if (target != null)
							{
								target.add(codePanel);
								target.add(filename);
							}
						}

						@Override
						protected CharSequence getURL()
						{
							return urlFor(SourcesPage.class,
								          SourcesPage.generatePageParameters(getPageTargetClass(),
									      item.getModel().getObject()));
						}

						@Override
						protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
						{
							super.updateAjaxAttributes(attributes);
							AjaxCallListener ajaxCallListener = new AjaxCallListener() {
								@Override
								public CharSequence getFailureHandler(Component component)
								{
									return "window.location=this.href;";
								}
							};
							attributes.getAjaxCallListeners().add(ajaxCallListener);
							attributes.getExtraParameters().put(PAGE_CLASS, "1");
						}
					};
					link.add(new Label("name", item.getDefaultModelObjectAsString()));
					item.add(link);
				}
			};
			add(lv);
		}
	}

	/**
	 * Container for displaying the source of the selected page, resource or other element from the
	 * package.
	 */
	public class CodePanel extends WebMarkupContainer
	{
		/**
		 * Constructor.
		 * 
		 * @param id
		 *            the component id
		 */
		public CodePanel(String id)
		{
			super(id);
			Label code = new Label("code", new SourceModel());
			code.setEscapeModelStrings(false);
			code.setOutputMarkupId(true);
			add(code);
		}
	}

	/**
	 * Parameter key for identifying the page class. UUID generated.
	 */
	public static final String PAGE_CLASS = SourcesPage.class.getSimpleName() + "_class";

	/**
	 * Parameter key for identify the name of the source file in the package.
	 */
	public static final String SOURCE = "source";

	/**
	 * The selected name of the packaged resource to display.
	 */
	private String name;

	private transient Class<? extends Page> page;

	/**
	 * The panel for setting the ajax calls.
	 */
	private final Component codePanel;

	private final Label filename;

	/**
	 * Sets the name.
	 * 
	 * @param name
	 *            the name to set.
	 */
	public void setName(String name)
	{
		this.name = name;
	}

	/**
	 * Gets the name.
	 * 
	 * @return the name.
	 */
	public String getName()
	{
		return name;
	}


	/**
	 * 
	 * Construct.
	 * 
	 * @param <C>
	 * @param params
	 */
	public <C extends Page> SourcesPage(final PageParameters params)
	{
		filename = new Label("filename", new AbstractReadOnlyModel<String>()
		{

			@Override
			public String getObject()
			{
				return name != null ? name : getPage().getRequest()
					.getRequestParameters()
					.getParameterValue(SOURCE)
					.toOptionalString();
			}

		});
		filename.setOutputMarkupId(true);
		add(filename);
		codePanel = new CodePanel("codepanel").setOutputMarkupId(true);
		add(codePanel);
		add(new FilesBrowser("filespanel"));
	}

	/**
	 * 
	 * @param page
	 * @return PageParamets for reconstructing the bookmarkable page.
	 */
	public static PageParameters generatePageParameters(Page page)
	{
		return generatePageParameters(page.getClass(), null);
	}

	/**
	 * 
	 * @param clazz
	 * @param fileName
	 * @return PageParameters for reconstructing the bookmarkable page.
	 */
	public static PageParameters generatePageParameters(Class<? extends Page> clazz, String fileName)
	{
		PageParameters p = new PageParameters();
		p.set(PAGE_CLASS, clazz.getName());
		if (fileName != null)
			p.set(SOURCE, fileName);
		return p;
	}

	private String getPageParam()
	{
		return getPage().getRequest()
			.getRequestParameters()
			.getParameterValue(PAGE_CLASS)
			.toOptionalString();
	}

	private Class<? extends Page> getPageTargetClass()
	{
		if (page == null)
		{
			try
			{
				String pageParam = getPageParam();
				if (pageParam == null)
				{
					if (log.isErrorEnabled())
					{
						log.error("key: " + PAGE_CLASS + " is null.");
					}
					getRequestCycle().replaceAllRequestHandlers(
						new ErrorCodeRequestHandler(404,
							"Could not find sources for the page you requested"));
				}
				if (!pageParam.startsWith("org.apache.wicket.examples"))
				{
					if (log.isErrorEnabled())
					{
						log.error("user is trying to access class: " + pageParam +
							" which is not in the scope of org.apache.wicket.examples");
					}
					throw new UnauthorizedInstantiationException(getClass());
				}
				page = (Class<? extends Page>)Class.forName(getPageParam());
			}
			catch (ClassNotFoundException e)
			{
				getRequestCycle().replaceAllRequestHandlers(
					new ErrorCodeRequestHandler(404,
						"Could not find sources for the page you requested"));
			}
		}
		return page;
	}
}


/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.examples.ajax.builtin;

import java.util.TimeZone;

import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.util.time.Duration;


/**
 * A world clock example page. Demonstrates timer behavior as well as multiple component update.
 * 
 * @author Igor Vaynberg (ivaynberg)
 */
public class WorldClockPage extends BasePage
{
    /**
     * Constructor
     */
    public WorldClockPage()
    {
        // create clock components for different timezones
        final Clock la = new Clock("la", TimeZone.getTimeZone("America/Los_Angeles"));
        final Clock ny = new Clock("ny", TimeZone.getTimeZone("America/New_York"));
        final Clock moscow = new Clock("moscow", TimeZone.getTimeZone("Europe/Moscow"));
        final Clock prague = new Clock("prague", TimeZone.getTimeZone("Europe/Prague"));
        final Clock london = new Clock("london", TimeZone.getTimeZone("Europe/London"));

        // make components print out id attrs so they can be updated via ajax
        la.setOutputMarkupId(true);
        ny.setOutputMarkupId(true);
        moscow.setOutputMarkupId(true);
        prague.setOutputMarkupId(true);
        london.setOutputMarkupId(true);

        // add the components to the container and add a markup id setter to
        // each component.
        add(la);
        add(ny);
        add(moscow);
        add(prague);
        add(london);

        // add the timer behavior to the page and make it update all
        // other components as well
        add(new AbstractAjaxTimerBehavior(Duration.seconds(1))
        {
            /**
             * @see org.apache.wicket.ajax.AbstractAjaxTimerBehavior#onTimer(org.apache.wicket.ajax.AjaxRequestTarget)
             */
            @Override
            protected void onTimer(AjaxRequestTarget target)
            {
                target.add(la);
                target.add(ny);
                target.add(moscow);
                target.add(prague);
                target.add(london);
            }
        });
    }
}

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.examples.ajax.builtin;

import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.extensions.ajax.markup.html.form.upload.UploadProgressBar;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.form.upload.FileUpload;
import org.apache.wicket.markup.html.form.upload.FileUploadField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.lang.Bytes;
import org.apache.wicket.validation.validator.StringValidator;

/**
 * Demos ajax handling of a multipart form
 * 
 * @author igor.vaynberg
 */
public class FileUploadPage extends BasePage
{
    private static final long serialVersionUID = 1L;

    private final FileUploadField file;
    private final TextField<String> text;

    /**
     * Constructor
     */
    public FileUploadPage()
    {

        // create a feedback panel
        final Component feedback = new FeedbackPanel("feedback").setOutputMarkupId(true);
        add(feedback);

        // create the form
        final Form<?> form = new Form<Void>("form")
        {
            private static final long serialVersionUID = 1L;

            /**
             * @see org.apache.wicket.markup.html.form.Form#onSubmit()
             */
            @Override
            protected void onSubmit()
            {
                // display uploaded info
                info("Text: " + text.getModelObject());
                FileUpload upload = file.getFileUpload();
                if (upload == null)
                {
                    info("No file uploaded");
                }
                else
                {
                    info("File-Name: " + upload.getClientFileName() + " File-Size: " +
                        Bytes.bytes(upload.getSize()).toString());
                }
            }
        };
        form.setMaxSize(Bytes.megabytes(1));
        add(form);

        // create a textfield to demo non-file content
        form.add(text = new TextField<String>("text", new Model<String>()));
        text.add(StringValidator.minimumLength(2));

        // create the file upload field
        form.add(file = new FileUploadField("file"));

        form.add(new Label("max", new AbstractReadOnlyModel<String>()
        {
            private static final long serialVersionUID = 1L;

            @Override
            public String getObject()
            {
                return form.getMaxSize().toString();
            }
        }));

        form.add(new UploadProgressBar("progress", form, file));

        // create the ajax button used to submit the form
        form.add(new AjaxButton("ajaxSubmit")
        {
            private static final long serialVersionUID = 1L;

            @Override
            protected void onSubmit(AjaxRequestTarget target, Form<?> form)
            {
                info("This request was processed using AJAX");

                // ajax-update the feedback panel
                target.add(feedback);
            }

            @Override
            protected void onError(AjaxRequestTarget target, Form<?> form)
            {
                // update feedback to display errors
                target.add(feedback);
            }

        });
    }
}


/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.examples.ajax.builtin;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormSubmitBehavior;
import org.apache.wicket.examples.guestbook.Comment;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.basic.MultiLineLabel;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;


/**
 * Ajax enabled example for the guestbook.
 * 
 * @author Martijn Dashorst
 */
public class GuestBook extends BasePage
{
    /** A global list of all comments from all users across all sessions */
    public static final List<Comment> commentList = new ArrayList<Comment>();

    /** The list view that shows comments */
    private final ListView<Comment> commentListView;
    /** Container for the comments, used to update the listview. */
    private final WebMarkupContainer comments;

    /** The textarea for entering the comments, is updated in the ajax call. */
    private Component text;

    /**
     * Constructor.
     */
    public GuestBook()
    {
        // Add comment form
        CommentForm commentForm = new CommentForm("commentForm");
        add(commentForm);

        // the WebMarkupContainer is used to update the listview in an ajax call
        comments = new WebMarkupContainer("comments");
        add(comments.setOutputMarkupId(true));

        // Add commentListView of existing comments
        comments.add(commentListView = new ListView<Comment>("comments",
            new PropertyModel<List<Comment>>(this, "commentList"))
        {
            @Override
            public void populateItem(final ListItem<Comment> listItem)
            {
                final Comment comment = listItem.getModelObject();
                listItem.add(new Label("date", new Model<Date>(comment.getDate())));
                listItem.add(new MultiLineLabel("text", comment.getText()));
            }
        });

        // we need to cancel the standard submit of the form in the onsubmit
        // handler,
        // otherwise we'll get double submits. To do so, we return false after
        // the
        // ajax submit has occurred.

        // The AjaxFormSubmitBehavior already calls the onSubmit of the form,
        // all
        // we need to do in the onSubmit(AjaxRequestTarget) handler is do our
        // Ajax
        // specific stuff, like rendering our components.
        commentForm.add(new AjaxFormSubmitBehavior(commentForm, "submit")
        {
            @Override
            protected void onSubmit(AjaxRequestTarget target)
            {
                // add the list of components that need to be updated
                target.add(comments);
                target.add(text);

                // focus the textarea again
                target.appendJavaScript("document.getElementById('" + text.getMarkupId() +
                    "').focus();");
            }

            @Override
            protected void onError(AjaxRequestTarget target)
            {
            }
        });
    }

    /**
     * A form that allows a user to add a comment.
     * 
     * @author Jonathan Locke
     */
    public final class CommentForm extends Form<Comment>
    {
        /**
         * Constructor
         * 
         * @param id
         *            The name of this component
         */
        public CommentForm(final String id)
        {
            // Construct form with no validation listener
            super(id, new CompoundPropertyModel<Comment>(new Comment()));

            // Add text entry widget
            text = new TextArea<String>("text").setOutputMarkupId(true);
            add(text);
        }

        /**
         * Show the resulting valid edit
         */
        @Override
        public final void onSubmit()
        {
            // Construct a copy of the edited comment
            final Comment comment = getModelObject();
            final Comment newComment = new Comment(comment);

            // Set date of comment to add
            newComment.setDate(new Date());

            // Add the component we edited to the list of comments
            commentList.add(0, newComment);

            // Clear out the text component
            comment.setText("");
        }
    }

    /**
     * Clears the comments.
     */
    public static void clear()
    {
        commentList.clear();
    }
}
